Трудоустройство и зарплаты выпускников российских вузов

Автор

Автор анализа Михаил Бельведерский

Код
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.offsetbox import OffsetImage, AnnotationBbox
import seaborn as sns
import matplotlib.pyplot as plt
import seaborn as sns

import itertools

import plotting_settings
plotting_settings.set_mpl()

from ru_graduates.map_figure import mapFigure
import plotly.io as pio
pio.renderers.default = "notebook"
from IPython.display import display

Данные

Данные о трудоустройстве и зарплатах выпускников российских организаций среднего специального и высшего образования по направлениям подготовки и уровням образования. Данные о трудоустройстве и приведены по состоянию на 31.12.2023, о зарплате — за 12 месяцев 2023 года.

Источник: Роструд, обработка: «Если быть точным»

В датасете нас будут интересовать:

  • наименование региона или федерального округа
  • направление подготовки (специализация)
  • средняя зарплата
  • пол выпускника

Трудоустройство и зарплаты выпускников по направлениям подготовки//Роструд; обработка «Если быть точным», 2024. Условия использования: Creative Commons BY 4.0. URL: https://tochno.st/datasets/graduates_fields

Код
smaller_df = pd.read_csv(
    'data/data_graduates_specialty_125_v20240709_csv/data_graduates_study_area_125_v20240611.csv', sep=';'
    )

fed_dist_bachelor_df = smaller_df.query(
    'object_level == "Федеральный округ" &'
    'education_level == "Бакалавриат, специалитет" &'
    'gender == "Всего"'
    )

fed_dist_master_df = smaller_df.query(
    'object_level == "Федеральный округ" &'
    'education_level == "Магистратура" &'
    'gender == "Всего"'
    )

print('Пропуски в данных:', '\n')
for column, n_empty in fed_dist_master_df.isna().sum().items():
    if n_empty > 0:
        print(
            f'Для выпускников магистратуры в данных о зарплате пропущено {n_empty} из {len(fed_dist_master_df)} значений'
            f' ({n_empty / len(fed_dist_master_df):.0%})'
            )
        
fed_dist_master_df[fed_dist_master_df['average_salary'].isna()][
    ['object_name', 'study_area', 'average_salary']
    ].sort_values('study_area', ascending=True).reset_index(drop=True)
Пропуски в данных: 

Для выпускников магистратуры в данных о зарплате пропущено 8 из 307 значений (3%)
object_name study_area average_salary
0 Южный федеральный округ Здравоохранение и медицинские науки NaN
1 Южный федеральный округ Здравоохранение и медицинские науки NaN
2 Южный федеральный округ Здравоохранение и медицинские науки NaN
3 Приволжский федеральный округ Здравоохранение и медицинские науки NaN
4 Уральский федеральный округ Здравоохранение и медицинские науки NaN
5 Уральский федеральный округ Здравоохранение и медицинские науки NaN
6 Дальневосточный федеральный округ Здравоохранение и медицинские науки NaN
7 Северо-Кавказский федеральный округ Искусство и культура NaN
Ограничения

В датасете есть информация о выпускниках разных лет — с 2019 по 2023. Размеры выборки не позволяют ограничиться только самыми свежими данными и замерять значения метрик, например, по отдельным федеральным округам и специальностям.

Код
russia_map = mapFigure()
import plotly.express as px

regions = pd.read_parquet("data/regions/russia_regions.parquet")
fo_list = list(regions['federal_district'].unique())
colors = px.colors.qualitative.Pastel1

for i, r in regions.iterrows():
    # popul_text = f"Население: <b>{r.population:_} </b>".replace('_', ' ')
    text = f'<b>{r.federal_district} ФО<br></b>{r.region}' #  ФО<br>{popul_text}
    russia_map.update_traces(selector=dict(name=r.region),
        text=text,
        fillcolor=colors[fo_list.index(r.federal_district)])
russia_map.show()

Посмотрим, выпускники каких специальностей зарабатывают больше всего в разных федеральных округаз. В топах по округам представлены выпускники следующих спецальностей:

инженерных и технических

математических и естестеннонаучных

медицинских

общественно-научных

сельскозозяйственных

Код
# Custom formatter function
def format_yticks(value, _):
    if value >= 1000:
        return f'{int(value/1000)}K'
    else:
        return f'{int(value)}'


def plot_top_salaries_by_fd(df, no_med=False):

    # Unique object names to iterate through
    unique_objects = df['object_name'].unique()

    # Setting up the grid (2 rows x 4 columns)
    fig, axes = plt.subplots(2, 4, figsize=(8, 7))
    axes = axes.flatten()  # Flatten the grid to make iteration easier

    # Define a color dictionary for the bars
    color_dict = {
    'Инженерное дело, технологии и технические науки': "#1E88E5",
    'Здравоохранение и медицинские науки': "#13B755",
    'Математические и естественные науки': "#ff0d57",
    'Сельское хозяйство и сельскохозяйственные науки': "#7C52FF",
    'Науки об обществе': "#FFC000",
    'Искусство и культура': "#00AEEF",
    'Образование и педагогические науки': "C2"
    }

    emoji_dict = {
    'Инженерное дело, технологии и технические науки': 'engineer',
    'Здравоохранение и медицинские науки': 'doctor',
    'Математические и естественные науки': 'nerd',
    'Сельское хозяйство и сельскохозяйственные науки': 'rural',
    'Науки об обществе': 'social',
    'Искусство и культура': 'art',
    'Образование и педагогические науки': 'books'
    }

    for ax_index, fed_dist in enumerate(unique_objects):
        if ax_index >= len(axes):
            break  # In case there are more unique objects than subplots

        fed_df = df.query(f'object_name == "{fed_dist}"')
        if no_med:
            fed_df = fed_df.query(
                'study_area != "Здравоохранение и медицинские науки"'
                )
        fed_stat_df = (
            fed_df.groupby('study_area')['average_salary']
            .median().nlargest(3)
        ).reset_index()  # Resetting index to get a DataFrame

        ax = axes[ax_index]  # Selecting the corresponding subplot

        if ax_index not in [0, 4]:
            for label in ax.get_yticklabels():
                label.set_alpha(0.5)
        
        # Create a color mapping for hue
        fed_stat_df['color'] = fed_stat_df['study_area'].map(color_dict)
        # fed_master_stat_df['color'] = fed_master_stat_df['study_area'].map(color_dict)
        # Plot with hue and disable legend
        sns.barplot(
            x='study_area', y='average_salary', data=fed_stat_df, ax=ax,
            hue='color', palette=fed_stat_df['color'].tolist(),
            legend=False, alpha=0.7)
        
        # Setting the title for each subplot
        ax.set_title(fed_dist.replace('федеральный округ', ''), fontsize=16, y=1.05, x=0.7)
        ax.set_ylim(0, 1.7e5)

        ax.set_ylabel('')
        ax.set_xlabel('')

        # ax.set_xlabel('Специальность')

        # Overlaying the image on top of each bar
        for i, bar in enumerate(ax.patches):
            x0 = bar.get_x() + bar.get_width() / 2.0  # Center the image horizontally on the bar
            y0 = bar.get_height()  # Position the image at the top of the bar

            bar_legend = fed_stat_df.iloc[i].study_area
            image_file = f'{emoji_dict.get(bar_legend)}.png'
            image = plt.imread(f'data/img/{image_file}')
            image_box = OffsetImage(image, zoom=0.15)  # Adjust zoom as needed
            ab = AnnotationBbox(image_box, (x0, y0), frameon=False, xycoords='data', box_alignment=(0.5, 0.0))
            ax.add_artist(ab)

            # Turn off the upper and right frame edges
            ax.tick_params(axis='x', bottom=False)

            # Apply the custom formatter to the y-axis
            ax.yaxis.set_major_formatter(plt.FuncFormatter(format_yticks))

        ax.set_xticklabels([])

        # Adding a common text label for the entire figure
        fig.text(0.5, 0.005, 'Специальность выпускника', ha='center', va='center', fontsize=20, fontweight='bold')
        fig.text(0.001, 0.45, 'Средняя зарплата (рубли)', ha='center', va='center', fontsize=20, fontweight='bold', rotation=90)

    # Adjust layout and show the plot
    plt.tight_layout()
    plt.show()
Код
plot_top_salaries_by_fd(fed_dist_bachelor_df)

Код
plot_top_salaries_by_fd(fed_dist_master_df)

cреди выпускников бакалавриата и специалитета

  • Только выпускники инженерных и технических специальностей входят в топ-3 по среднему размеру зарплат во всех федеральных округах

  • Только в Центральном округе выпускники математических и естестеннонаучных специальностей получают больше остальных

cреди выпускников магистратуры

  • Наиболее заметный рост зарплаты по отношению к выпускникам бакалавриата или специалитета наблюдается в Центральном и Северо-Западном федеральном округе

  • Наименьший прирост зарплаты у магистров по сравнению с бакалаврами/специалистами наблюдается в Южном, Приволжском и Северо-Кавказском округах

  • Аномально большой прирост зарплаты наблюдается у выпускников медицинских специальностей, особенно он заметен в Сибирском и Дальневосточном округах

  • Магистры медицинских специальностей не входят в топ-3 по уровню зарплат в Южном и Северо-Кавказском округах, скорее всего связано с пропусками в данных

Пропуски в данных

В Южном и Северо-Кавказском округах отсутствует информация о зарплате магистров медицинских наук:

Код
df1 = pd.DataFrame(fed_dist_bachelor_df.query(
    'object_name == "Южный федеральный округ"'
    '& study_area == "Здравоохранение и медицинские науки"'
    )['average_salary'])

df2 = pd.DataFrame(fed_dist_master_df.query(
    'object_name == "Южный федеральный округ"'
    '& study_area == "Здравоохранение и медицинские науки"'
    )['average_salary'])

df3 = pd.DataFrame(fed_dist_bachelor_df.query(
    'object_name == "Северо-Кавказский федеральный округ"'
    '& study_area == "Здравоохранение и медицинские науки"'
    )['average_salary'])

df4 = pd.DataFrame(fed_dist_master_df.query(
    'object_name == "Северо-Кавказский федеральный округ"'
    '& study_area == "Здравоохранение и медицинские науки"'
    )['average_salary'])
Код
```{python}
#| label: tbl-ref
#| tbl-cap: "Южный федеральный округ"
#| tbl-subcap:
#|  - "Бакалавры/специалисты"
#|  - "Магистры"
#| layout-ncol: 2

display(df1)
display(df2)
```
Таблица 1: Южный федеральный округ
(a) Бакалавры/специалисты
average_salary
3122 72838.30634
3130 70779.21762
3138 57534.04568
3146 48477.49281
3154 41583.38503
(b) Магистры
average_salary
3169 NaN
3177 NaN
3185 NaN
Код
```{python}
#| label: tbl-ref-2
#| tbl-cap: "Северо-Кавказский федеральный округ"
#| tbl-subcap:
#|  - "Бакалавры/специалисты"
#|  - "Магистры (нет записей)"
#| layout-ncol: 2

display(df3)
display(df4)
```
Таблица 2: Северо-Кавказский федеральный округ
(a) Бакалавры/специалисты
average_salary
3265 68271.08231
3273 64426.35973
3281 59316.38948
3289 50192.02503
3297 43859.00063
(b) Магистры (нет записей)
average_salary

Комментарий Алены Манузиной («Если быть точным»):

По федеральным округам — надо проверять, но учитывая размер набора, можно предположить, что в ЮФО и СКФО медицинской магистратуры просто нигде нет. Если верить этим товарищам, такая магистратура есть в Москве, СПб, Казани, Томске, Рязани, Тамбове и Владивостоке.


Средние зарплаты выпускников

по стране в целом для выпускников бакалавриата/специалитета и магистратуры:

Код
def salary_by_area_stat(df):
    return pd.DataFrame(df.groupby('study_area').agg(
        average_salary=('average_salary', 'median'),
        group_size=('average_salary', 'count'))
        )

def bachelor_vs_master(bachelor_df, master_df, ax, stat_func, no_med=False):

    bachelor_stat_df = stat_func(bachelor_df)
    master_stat_df = stat_func(master_df)

    total_stat_df = bachelor_stat_df.merge(
        master_stat_df, on='study_area', how='left',
        suffixes=['_bachelor', '_master']
        ).sort_values(by='average_salary_master', ascending=False)
    
    if no_med:
        total_stat_df = total_stat_df.query(
            'study_area != "Здравоохранение и медицинские науки"'
            )

    total_stat_df.index = [' и\n'.join(i.split(' и ')) for i in total_stat_df.index]
    total_stat_df.index.name = 'study_area'
    
    sns.scatterplot(
        total_stat_df, x='average_salary_bachelor', y='study_area', s=70,
        ax=ax, zorder=5, color="#00AEEF", label='Бакалавры и\nспециалисты', lw=2
        )
    
    sns.scatterplot(
        total_stat_df, x='average_salary_master', y='study_area', s=70,
        ax=ax, zorder=5, color='#ff0d57', label='Магистры', lw=2,
        marker='s'
        )
    
    for area, row in total_stat_df.iterrows():
        ax.plot(
            (row.average_salary_bachelor, row.average_salary_master),
            (area, area), color='lightgray', lw=3
            )

    ax.xaxis.set_major_formatter(plt.FuncFormatter(format_yticks))
    ax.set_xlim(3e4, 1.5e5)
    ax.grid(alpha=.3)
    ax.legend(loc='lower right')

    ax.set_xlabel('Средняя зарплата (рубли)')
    ax.set_ylabel('')

    return (bachelor_stat_df, master_stat_df, total_stat_df), ax

fig, ax = plt.subplots(figsize=(6, 7))
dfs, ax = bachelor_vs_master(
    fed_dist_bachelor_df, fed_dist_master_df, ax, stat_func=salary_by_area_stat
    )

Среднее значение для зарплаты магистров-медиков выглядит аномально большим. Комментарий Алены Манузиной («Если быть точным»):

Формально медицинская магистратура существует (направления есть в перечне и, например, здесь — все, что с кодами на 32, 33 и 34). Другое дело, что это подготовка скорее к управленческим позициям, поэтому на них идут уже взрослые и опытные люди (это единственная группа со средним возрастом выпускников 35+), отсюда и разница в зарплате между выпускниками бакалавриата/специалитета и магистратуры. Плюс наборы очень маленькие — по данным Роструда это 100-200 человек на всю страну в разные годы, поэтому расчеты на основе этих данных и сопоставление с другими областями образования могут быть очень ненадежными.

Код
fig, ax = plt.subplots(figsize=(6, 7))

QUERY = 'study_area != "Здравоохранение и медицинские науки"'

dfs, ax = bachelor_vs_master(
    fed_dist_bachelor_df.query(QUERY),
fed_dist_master_df.query(QUERY),
    ax, stat_func=salary_by_area_stat
    )

  • Больше всего в Росии в среднем зарабатывают выпускники инженерных и технических специальностей (в среднем от 90 до 100 тысяч рублей в месяц)

  • На втором месте выпускники математических и естественных наук (от 70 до 95 тысяч), на третьем — общественно-научных специальностей (от 60 до 83 тысяч)

  • Выпускники педагогических, сельскохозяйственных, гуманитарных направлений и специальностей, связанных с искуством и культурой получают примерно одинаково — около 60 тысяч рублей в месяц

Теперь обновим топ специальностей.

Топ-3 специальностей по среднему размеру зарплат по федеральным округам

Из выборки исключены данные о выпускниках медицинских специальностей.

В топах по округам представлены выпускники следующих спецальностей:

инженерных и технических

математических и естестеннонаучных

общественно-научных

сельскозозяйственных

педагогических

Код
plot_top_salaries_by_fd(fed_dist_bachelor_df, no_med=True)

Код
plot_top_salaries_by_fd(fed_dist_master_df, no_med=True)

Топ-3 специальностей по размеру зарплат в разных федеральных округах для выпускников магистратуры стал гораздо более похож на аналогичное распределение для выпускников бакалавриата/специалитета после исключения смещённых данных о выпускниках медицинских специальностей.

cреди выпускников бакалавриата и специалитета

  • Только выпускники инженерных и технических специальностей входят в топ-3 по среднему размеру зарплат во всех федеральных округах

  • Только в Центральном округе выпускники математических и естестеннонаучных специальностей получают больше остальных

cреди выпускников магистратуры

  • Наиболее заметный рост зарплаты по отношению к выпускникам бакалавриата или специалитета наблюдается в Центральном и Северо-Западном федеральном округе

  • Наименьший прирост зарплаты у магистров по сравнению с бакалаврами/специалистами наблюдается в Южном, Приволжском и Северо-Кавказском округах


Как меняется зарплата выпускников в зависимости от количества лет с момента выпуска

Бакалавры и специалисты

Код
def plot_career_dynamics(df):

    df = df.query('study_area != "Здравоохранение и медицинские науки"')

    grouped_df = df.groupby(['study_area', 'year']).agg(
        average_salary=('average_salary', 'median'),
        group_size=('average_salary', 'count')
    ).reset_index()

    # Pivot the DataFrame
    pivot_df = grouped_df.pivot_table(
        index='study_area',
        columns='year',
        values='average_salary'
    )

    # Flatten the columns
    pivot_df.columns = [abs(col - 2023) for col in pivot_df.columns]

    # Reset index to make 'study_area' a column again
    pivot_df = pivot_df.reset_index()

    pivot_df = pivot_df.set_index('study_area')

    _, ax = plt.subplots(figsize=(6, 6))

    areas_of_interest = [
        'Инженерное дело, технологии и технические науки',
        'Математические и естественные науки',
        'Науки об обществе'
    ]
    
    line_styles = ['--', '-.', '-']
    line_style_cycle = itertools.cycle(line_styles)
    
    for area in list(pivot_df.index):
        if area in areas_of_interest:
            area_label = ' и\n'.join(area.split(' и '))
            ax.plot(pivot_df.loc[area], label=area_label, ls=next(line_style_cycle))
        else:
            ax.plot(pivot_df.loc[area], color='gray', alpha=.2, label='Другие специальности')

    handles, labels = ax.get_legend_handles_labels()
    unique = dict(zip(labels, handles))

    # Define the desired order of the legend entries
    desired_order = [
        'Инженерное дело, технологии и\nтехнические науки',
        'Математические и\nестественные науки',
        'Науки об обществе',
        'Другие специальности'
    ]

    # Reorder the handles and labels according to the desired order
    ordered_handles = [unique[label] for label in desired_order if label in unique]
    ordered_labels = [label for label in desired_order if label in unique]

    ax.legend(ordered_handles, ordered_labels, fontsize=12, loc='upper left')
    ax.yaxis.set_major_formatter(plt.FuncFormatter(format_yticks))

    ax.grid(alpha=.3)
    ax.set_xlabel('Время с момента выпуска (годы)')
    ax.set_ylabel('Средняя зарплата (рубли)')
    ax.set_xlim(0, 4)
    ax.set_ylim(40e3, 160e3)

    return grouped_df

_ = plot_career_dynamics(fed_dist_bachelor_df)

Код
male_bahelor_df = smaller_df.query(
    'object_level == "Федеральный округ" &'
    'education_level == "Бакалавриат, специалитет" &'
    'gender == "Мужской"'
    )

_ = plot_career_dynamics(male_bahelor_df)

Код
fem_bahelor_df = smaller_df.query(
    'object_level == "Федеральный округ" &'
    'education_level == "Бакалавриат, специалитет" &'
    'gender == "Женский"'
    )

_ = plot_career_dynamics(fem_bahelor_df)

Магистры

Код
_ = plot_career_dynamics(fed_dist_master_df)

Код
male_master_df = smaller_df.query(
    'object_level == "Федеральный округ" &'
    'education_level == "Магистратура" &'
    'gender == "Мужской"'
    )

_ = plot_career_dynamics(male_master_df)

Код
fem_master_df = smaller_df.query(
    'object_level == "Федеральный округ" &'
    'education_level == "Магистратура" &'
    'gender == "Женский"'
    )

_ = plot_career_dynamics(fem_master_df)